/*
 * Copyright 2014 Щекотова Ангелина Александровна.
 * Этот файл — часть J4JClientServer Framework
 * J4JClientServer Framework - свободная программа: вы можете перераспространять ее и/или изменять
 * ее на условиях Меньшей Стандартной общественной лицензии GNU в том виде, в каком
 * она была опубликована Фондом свободного программного обеспечения; либо
 * версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.
 *
 * J4JClientServer Framework распространяется в надежде, что она будет полезной,
 * но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА
 * или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Меньшей Стандартной
 * общественной лицензии GNU.
 *
 * Вы должны были получить копию Меньшей Стандартной общественной лицензии GNU
 * вместе с этой программой. Если это не так, см.
 * <http://www.gnu.org/licenses/>.
 */

package ru.java4java.ClientServerFramework;

import ru.java4java.ClientServerFramework.impl.*;
import ru.java4java.ClientServerFramework.interfaces.*;
import ru.java4java.utils.Counter;
import ru.java4java.utils.*;
import org.apache.log4j.Logger;

import java.io.*;
import java.net.*;

/**<pre>
 *
 * Обработчик клиентов.
 * Для запуска объекта этого класса используйте {@link java.lang.Thread}
 *
 * <b>Смотрите:</b>
 *   {@link ru.java4java.ClientServerFramework.interfaces.SocketHandlerListener}
 *   {@link ru.java4java.ClientServerFramework.interfaces.ExceptionListener}
 * </pre>
 */
public class SocketHandler implements Runnable, Closeable {

    private static final Logger log = Logger.getLogger(SocketHandler.class);

    private static int handlerNumber = 1;

    private Socket socket;
    private InetAddress inetAddress;
    private int port;

    private Thread createdThread = null;
    private Thread runThread = null;
    private boolean shutdown = false;

    private Counter errorCounter;

    private volatile Packet outgoingMessage = null;
    private volatile Packet incomingMessage = null;

    private SocketHandlerOptions options = new SocketHandlerOptions();

    /**
     * Создает Клиент-Обработчик.
     * @param host Хотс подключения, например: 127.0.0.1
     * @param port порт подключения от 0 до 65535. Например: 9797
     * @exception  UnknownHostException если IP адрес не найден для заданного хоста.
     *             Смотрите: {@link java.net.InetAddress#getByName(String)}
     * @exception  SecurityException если "security manager" существует
     *             и проверка подсоединения не разрешает выполнить данную операцию.
     *             Смотрите: {@link java.net.InetAddress#getByName(String)}
     */
    public SocketHandler(String host, int port) throws UnknownHostException {
        this();
        this.socket = new Socket();
        this.port = port;
        this.inetAddress = InetAddress.getByName(host);
    }

    /**
     * Создает Клиент-Обработчик.
     * @param socket {@link java.net.Socket} - укажите сокет который вы хотите обрабатывать
     */
    public SocketHandler(Socket socket) {
        this();
        this.socket = socket;
        this.port = socket.getPort();
        this.inetAddress = socket.getInetAddress();
    }

    private SocketHandler() {
        this.errorCounter = new Counter(0, 5).autoReset(false);
        this.createdThread = Thread.currentThread();
    }

    /** <pre>
     * Подключить этого клиента к серверу указанному при создании объекта,
     * если соединение не было установлено до этого.
     * </pre>
     * @throws  IOException возникает при ошибке ввода\вывода.
     *                      Смотрите: {@link java.net.Socket#connect(java.net.SocketAddress)}
     * @throws  java.nio.channels.IllegalBlockingModeException
     *          Смотрите: {@link java.net.Socket#connect(java.net.SocketAddress)}
     * @throws  IllegalArgumentException
     *          Смотрите: {@link java.net.InetSocketAddress#InetSocketAddress(java.net.InetAddress, int)}
     */
    public void connect() throws IOException {
        if (!socket.isConnected()) {
            InetSocketAddress socketAddress = new InetSocketAddress(inetAddress, port);
            socket.connect(socketAddress);
        }
    }

    /**<pre>
     * Возвращает настройки обработчика.
     * </pre>
     * @return  {@link SocketHandlerOptions}
     */
    public SocketHandlerOptions options() { return options; }

    /**<pre>
     * Установит объект опций для обработчика
     * </pre>
     * @param options {@link SocketHandlerOptions}
     */
    public void setOptions(SocketHandlerOptions options) { this.options = options; }


    /**<pre>
     * Максимальное количество ошибок подряд.
     * Если будет превышен этот лимит, то обработчик закроет сокет и завершит свою работу.
     * Работает при использовании автоматической отправке и/или приема сообщений.
     * Смотрите:
     *  * {@link SocketHandler#options()}
     *  * {@link SocketHandlerOptions#setAutoSend(boolean)}
     *  * {@link SocketHandlerOptions#setAutoReceive(boolean)}
     * </pre>
     * @return возвращает целочисленное значение в виде "int" (Integer)
     * @see SocketHandler#setMaxErrorCount(int)
     */
    public int getMaxErrorCount() { return NumberUtils.long2int(errorCounter.getMax()); }

    /**<pre>
     * Устанавливает максимальное количество ошибок подряд.
     * Если будет превышен этот лимит, то обработчик закроет сокет и завершит свою работу.
     * Работает при использовании автоматической отправке и/или приема сообщений.
     * Смотрите:
     *  * {@link SocketHandler#options()}
     *  * {@link SocketHandlerOptions#setAutoSend(boolean)}
     *  * {@link SocketHandlerOptions#setAutoReceive(boolean)}
     * </pre>
     * @param count int value
     * @see SocketHandler#getMaxErrorCount()
     */
    public void setMaxErrorCount(int count) { errorCounter.setMax(count); }

    /** <pre>
     * Увеличивает текущее количество ошибок на единицу.
     * </pre>
     * @see SocketHandler#decrementErrorCount()
     * @see SocketHandler#resetErrorCount()
     */
    public void incrementErrorCount() { errorCounter.increment(); }

    /** <pre>
     * Уменьшает текущее количество ошибок на единицу.
     * </pre>
     * @see SocketHandler#incrementErrorCount()
     * @see SocketHandler#resetErrorCount()
     */
    public void decrementErrorCount() { errorCounter.decrement(); }

    /** <pre>
     * Сбрасывает счетчик ошибок.
     * </pre>
     * @see SocketHandler#incrementErrorCount()
     * @see SocketHandler#decrementErrorCount()
     */
    public void resetErrorCount() { errorCounter.reset(); }

    /**<pre>
     * Возвращает true, если количество ошибок перевалило за максимальное значение.
     * Смотрите:
     *  * {@link SocketHandler#getMaxErrorCount()}
     *  * {@link SocketHandler#setMaxErrorCount(int)}
     * </pre>
     * @return true/false
     */
    public boolean errorCountIsOverload() { return errorCounter.isOverload(); }

    /**
     * Можно запустить только через {@link java.lang.Thread} класс как новый поток.
     * Один экземпляр обработчика можно запустить только 1 раз.
     * @see java.lang.Thread
     * @see java.lang.Thread#Thread(Runnable target)
     * @see java.lang.Thread#Thread(Runnable target, String name)
     * @see java.lang.Thread#Thread(ThreadGroup group, Runnable target)
     * @see java.lang.Thread#Thread(ThreadGroup group, Runnable target, String name)
     * @see java.lang.Thread#Thread(ThreadGroup group, Runnable target, String name, long stackSize)
     * @deprecated используйте {@link java.lang.Thread} класс и метод {@link Thread#start()} для запуска нового потока.
     */
    @Override
    @Deprecated
    public void run() {
        if (!initThread()) return;
        boolean doPing = false; /* ping relay */
        try {
            SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(), SocketHandlerListenerAdapter.OperationType.BEFORE_START);
            while (!socket.isClosed() && !shutdown) {
                try {
                /* ************************************************************************* */
                /* Основная логика обработчика в потоке */
                /* ************************************************************************* */
                    if (doPing) {
                        doPing = false;
                        ping(options.getPingPacket());          /* <--- Ping */
                    } else {
                        if (options.isAutoReceive()) receive(); /* <--- Get incoming message */
                        if (options.isAutoSend())    send();    /* <--- Send outgoing message */
                    }
                    errorCounter.decrement(); /* Reset Errors */
                /* ************************************************************************* */
                }
                catch (EOFException e) {
                    if (listenException(ExceptionListener.Type.EOF_EXCEPTION, e)) throw new Shutdown();
                }
                catch (SocketTimeoutException e) {
                    if (options.getPingPacket() != null && !doPing) {
                        doPing = true;
                    } else {
                        if (listenException(ExceptionListener.Type.SOCKET_TIMEOUT_EXCEPTION, e)) throw new Shutdown();
                        log.error("SocketTimeoutException", e);
                        throw new Shutdown();
                    }
                }
                catch (SocketException e) {
                    if (listenException(ExceptionListener.Type.SOCKET_EXCEPTION, e)) throw new Shutdown();
                    String message = e.getMessage().trim();
                    if ("Connection reset".equals(message)) {
                        log.warn(socket + " Connection reset!");
                    } else {
                        log.error("SocketException", e);
                    }
                    throw new Shutdown();
                }
                catch (IOException e) {
                    if (listenException(ExceptionListener.Type.IO_EXCEPTION, e)) throw new Shutdown();
                    String msg = SocketUtils.isFullAvailable(socket); /* Validate Socket */
                    if (msg.isEmpty()) {
                        if (log.isTraceEnabled()) log.trace("IOException. Socket " + socket, e);
                        errorCounter.increment();
                    } else {
                        log.info(msg);
                        throw new Shutdown();
                    }
                }
                if (errorCounter.isOverload()) {
                    log.error("Error count > " + getMaxErrorCount()+". Shutdown...");
                    throw new Shutdown();
                }
            } /* <<--- End While */
        } catch (Shutdown ignored) {
        } finally {
            log.info("Close current Thread. Socket: " + socket);
            shutdown();
            log.info("Current Thread closed.");
        }

    }

    /**
     * @return {@link java.net.Socket} текущего обработчика
     */
    public Socket getSocket() { return socket; }

    @Deprecated  // Not is Deprecated :)
    public void changeSocket(Socket socket) { this.socket = socket; }

    /**<pre>
     * Возвращает {@link ru.java4java.ClientServerFramework.impl.Packet}
     * который должен будет отправлен при авто отправке
     * или при помощи метода {@link SocketHandler#send()}
     *
     * Для того, что бы отправка прошла успешно,
     * сначала установите пакет для отправки используя метод
     * {@link SocketHandler#setOutgoing(ru.java4java.ClientServerFramework.impl.Packet)}
     * </pre>
     * @return {@link ru.java4java.ClientServerFramework.impl.Packet}
     * @see ru.java4java.ClientServerFramework.interfaces.SocketHandlerListener
     */
    public Packet getOutgoing() { return outgoingMessage; }

    /**<pre>
     * Set outgoing message for send Response or Request to Server.
     * Idea: You can use it for encrypting your messages. ;)
     * </pre>
     * @param outgoing {@link ru.java4java.ClientServerFramework.impl.Packet}
     */
    public void setOutgoing(Packet outgoing) { this.outgoingMessage = outgoing; }

    /**<pre>
     * Возвратит пакет, который был получен при авто получении или при помощи вызова метода {@link SocketHandler#receive()}
     * </pre>
     * @return {@link ru.java4java.ClientServerFramework.impl.Packet}
     */
    public Packet getIncoming() { return incomingMessage; }

    /**<pre>
     * Установит переменную входящего сообщения.
     * Это может быть полезным, если вы хотите подготовить данные первым листенером для обработки последующим.
     * Например, это может пригодиться для расшифровки сообщения перед его обработкой.
     * </pre>
     * @param incoming {@link ru.java4java.ClientServerFramework.impl.Packet}
     */
    public void setIncoming(Packet incoming) { this.incomingMessage = incoming; }

    /**<pre>
     * Отправит пакет установленный методом {@link SocketHandler#setOutgoing(ru.java4java.ClientServerFramework.impl.Packet)}
     * Если отправлять нечего, метод просто бездействует.
     * Смотрите: {@link SocketHandler#getOutgoing()}, {@link SocketHandler#setOutgoing(ru.java4java.ClientServerFramework.impl.Packet)}
     * </pre>
     * @throws IOException if an I/O error occurs
     */
    public void send() throws IOException {
        /* Validate outgoing message */
        Packet outgoing = getOutgoing();
        if (outgoing == null) return;

        try {
            /* Before */
            SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(),
              SocketHandlerListenerAdapter.OperationType.BEFORE_SEND);

            /* Setup Writer */
            PacketWriter writer = getPacketWriter();
            writer.setOutputStream(socket.getOutputStream());

            /* Send Packet */
            writer.writePacket(outgoing);

            /* After */
            SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(),
              SocketHandlerListenerAdapter.OperationType.AFTER_SEND);
        } finally {
            /* Reset outgoing */
            setOutgoing(null);
        }
    }

    /** <pre>
     * Если вы используете этот метод, то слушатели исключений не будут вызваны.
     * Вместо этого метода, используйте {@link SocketHandler#receivePacket()}
     *
     * Этот метод получает {@link ru.java4java.ClientServerFramework.impl.Packet} и сохроняет его в переменную.
     * Смотрите: {@link SocketHandler#getIncoming()}, {@link SocketHandler#setIncoming(ru.java4java.ClientServerFramework.impl.Packet)}
     * </pre>
     * @throws IOException ошибки при получении собщения
     */
    public void receive() throws IOException {
        setIncoming(null);

        /* Before */
        SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(),
          SocketHandlerListenerAdapter.OperationType.BEFORE_GET);

        /* Setup Reader */
        PacketReader reader = getPacketReader();
        reader.setInputStream(socket.getInputStream());
        /* Get Message */
        Packet incoming  = reader.readPacket();

        setIncoming(incoming);

        /* After */
        SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(),
          SocketHandlerListenerAdapter.OperationType.AFTER_GET);
    }

    /**<pre>
     * Этот метод получает {@link ru.java4java.ClientServerFramework.impl.Packet}
     *   и возвращает его в качестве рузультата.
     * Если будет брошено исключение, то будет вызван слушатель исключений,
     *   таких как EOFException, SocketTimeoutException, SocketException, IOException.
     * Если слушатель исключения вернет положительный результат,
     *   то исключение будет проброшено выше.
     * </pre>
     * @return {@link ru.java4java.ClientServerFramework.impl.Packet} или null
     * @throws IOException ошибки ввода/вывода
     */
    public Packet receivePacket() throws IOException {
        try {
            receive();
        }
        catch (EOFException e) {
            if (listenException(ExceptionListener.Type.EOF_EXCEPTION, e)) throw e;
        }
        catch (SocketTimeoutException e) {
            if (listenException(ExceptionListener.Type.SOCKET_TIMEOUT_EXCEPTION, e)) throw e;
        }
        catch (SocketException e) {
            if (listenException(ExceptionListener.Type.SOCKET_EXCEPTION, e)) throw e;
        }
        catch (IOException e) {
            if (listenException(ExceptionListener.Type.IO_EXCEPTION, e)) throw e;
        }
        return getIncoming();
    }

    /**<pre>
     * Отправит {@link ru.java4java.ClientServerFramework.impl.Packet} получателю.
     * Если возникнет исключение, то будут вызван слушатель исключения, таких как SocketException и IOException.
     * Если слушатель исключения вернет положительный результат,
     * то исключение будет проброшено выше.
     * </pre>
     * @param packet {@link ru.java4java.ClientServerFramework.impl.Packet}
     * @throws IOException ошибки ввода/вывода
     */
    public void sendPacket(Packet packet) throws IOException {
        setOutgoing(packet);
        try {
            send();
        } catch (SocketException e) {
            if (listenException(ExceptionListener.Type.SOCKET_EXCEPTION, e)) throw e;
        } catch (IOException e) {
            if (listenException(ExceptionListener.Type.IO_EXCEPTION, e)) throw e;
        }
    }

    /**<pre>
     * Возвращает писателя пакетов.
     * Используйте это для отправки пакета.
     * Смотрите метод: {@link PacketWriter#writePacket(ru.java4java.ClientServerFramework.impl.Packet)}
     * </pre>
     * @return PacketWriter {@link ru.java4java.ClientServerFramework.interfaces.PacketWriter}
     */
    public PacketWriter getPacketWriter() {
        PacketWriter writer = this.options.getPacketWriter();
        if (writer == null) {
            writer = new BytePacketWriter();
        }
        return writer;
    }

    /**<pre>
     * Возвращает считывателя пакетов.
     * Если пакет не был установлен в настройках клиента,
     * то будет использоваться реализация считывателя по умолчанию.
     *
     * Используйте это для получения пакета.
     * Смотрите метод: {@link ru.java4java.ClientServerFramework.interfaces.PacketReader#readPacket()}
     * </pre>
     * @return {@link ru.java4java.ClientServerFramework.interfaces.PacketReader}
     */
    public PacketReader getPacketReader() {
        PacketReader reader = this.options.getPacketReader();
        if (reader == null) {
            reader = new BytePacketReader();
        }
        return reader;
    }

    /**<pre>
     * Смотрите: {@link SocketHandler#shutdown()}
     * </pre>
     */
    @Override
    @Deprecated // Not is Deprecated :)
    public final void close() {
        if (!socket.isClosed()) {
            SocketUtils.shutdownSocket(5000, socket);
        }
    }

    /**<pre>
     * Возвращает состояние сокета на данном обработчике.
     *
     * </pre>
     * @return true или false
     * @see java.net.Socket#isClosed()
     */
    public final boolean isClosed() {
        return socket.isClosed();
    }

    /**<pre>
     * Завершает работу текущего обработчика.
     * </pre>
     * @see java.net.Socket#close()
     */
    @SuppressWarnings("deprecation")
    public final void shutdown() {
        if (!shutdown) {
            SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(), SocketHandlerListenerAdapter.OperationType.BEFORE_SHUTDOWN);
            close();
            SocketHandlerListenerAdapter.runListeners(this, options.getSocketHandlerListeners(), SocketHandlerListenerAdapter.OperationType.AFTER_SHUTDOWN);
            errorCounter.reset();
            shutdown = true;
        }
    }

    private boolean initThread() {
        if (shutdown) throw new SocketHandlerRunException("The SocketHandler was already shutdown.");
        validateAndPrepareThread();
        try {
            connect();
        } catch (IOException e) {
            log.error("Connect Exception", e);
            shutdown();
            return false;
        }
        resetErrorCount();
        return true;
    }

    private void validateAndPrepareThread() {
        if (Thread.currentThread() == createdThread) {
            throw new SocketHandlerRunException("Use Thread class for run SocketHandler Class as Runnable class.");
        }
        if (runThread != null) {
            throw new SocketHandlerRunException("The SocketHandler was already running.");
        }
        runThread = Thread.currentThread();
        runThread.setName("SocketHandler-Thread-" + (handlerNumber++));
    }

    public long ping(Packet request) throws IOException {
        Counter.Timer timer = new Counter.Timer();
        long result = -1;
        if (request != null) {
            setOutgoing(request);

            timer.start();                    /* <--- Start Timer */

            send();    /* <--- Ping send    */
            receive(); /* <--- Ping receive */

            result = timer.stop().difference(); /* <--- Stop timer */

            /* Validate response */
            Packet response = getIncoming();
            if (response == null) {
                throw new PingException("Response packet is null.");
            }
            if (!request.equals(response)) {
                throw new PingException("The resulting packet is not equals packet of ping.");
            }
        }
        return result;
    }

    private static class Shutdown extends Exception {}

    private boolean listenException(ExceptionListener.Type type, Exception exception) {
        ExceptionListener listener = options.getExceptionListener();
        return listener != null && listener.exception(type, exception);
    }

}
